import type { EngravingSettings } from '@coderline/alphatab/EngravingSettings';
import type { Bar } from '@coderline/alphatab/model/Bar';
import type { Font } from '@coderline/alphatab/model/Font';
import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol';
import {
    BracketExtendMode,
    TrackNameMode,
    TrackNameOrientation,
    TrackNamePolicy
} from '@coderline/alphatab/model/RenderStylesheet';
import { type Track, TrackSubElement } from '@coderline/alphatab/model/Track';
import { NotationElement } from '@coderline/alphatab/NotationSettings';
import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas';
import type { RenderingResources } from '@coderline/alphatab/RenderingResources';
import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase';
import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer';
import type { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout';
import { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo';
import { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers';
import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff';
import { StaffTrackGroup } from '@coderline/alphatab/rendering/staves/StaffTrackGroup';
import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds';
import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper';
import { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds';
import { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/StaffSystemBounds';

/**
 * @internal
 */
export abstract class SystemBracket {
    public firstStaffInBracket: RenderStaff | null = null;
    public lastStaffInBracket: RenderStaff | null = null;
    public drawAsBrace: boolean = false;
    public braceScale: number = 1;
    public width: number = 0;
    public index: number = 0;
    public get canPaint(): boolean {
        return this.firstStaffInBracket !== null && this.lastStaffInBracket !== null;
    }

    public abstract includesStaff(s: RenderStaff): boolean;

    public finalizeBracket(smuflMetrics: EngravingSettings) {
        // systems with just a single staff do not have a bracket
        if (this.firstStaffInBracket === this.lastStaffInBracket) {
            this.width = 0;
            return;
        }

        // SMUFL: The brace glyph should have a height of 1em, i.e. the height of a single five-line stave, and should be scaled proportionally
        const bravuraBraceHeightAtMusicFontSize = smuflMetrics.glyphHeights.get(MusicFontSymbol.Brace)!;
        const bravuraBraceWidthAtMusicFontSize = smuflMetrics.glyphWidths.get(MusicFontSymbol.Brace)!;

        // normal bracket width
        if (this.drawAsBrace) {
            this.width = bravuraBraceWidthAtMusicFontSize;
        } else {
            this.width = smuflMetrics.bracketThickness;
        }
        if (!this.drawAsBrace || !this.firstStaffInBracket || !this.lastStaffInBracket) {
            return;
        }

        const firstStart: number = this.firstStaffInBracket.contentTop;
        const lastEnd: number = this.lastStaffInBracket.contentBottom;

        const requiredHeight = lastEnd - firstStart;
        const requiredScaleForBracket = requiredHeight / bravuraBraceHeightAtMusicFontSize;
        this.braceScale = requiredScaleForBracket;
        this.width = bravuraBraceWidthAtMusicFontSize * this.braceScale;
    }
}

/**
 * @internal
 */
class SingleTrackSystemBracket extends SystemBracket {
    protected track: Track;

    public constructor(track: Track) {
        super();
        this.track = track;
        this.drawAsBrace = SingleTrackSystemBracket.isTrackDrawAsBrace(track);
    }

    public override includesStaff(r: RenderStaff): boolean {
        return r.modelStaff.track === this.track;
    }

    public static isTrackDrawAsBrace(track: Track) {
        return track.staves.filter(s => s.showStandardNotation as boolean).length > 1;
    }
}

/**
 * @internal
 */
class SimilarInstrumentSystemBracket extends SingleTrackSystemBracket {
    public override includesStaff(r: RenderStaff): boolean {
        // allow merging on same track (for braces, percussion and items belonging together)
        if (r.modelStaff.track === this.track) {
            return true;
        }

        // braces are never cross-track
        if (this.drawAsBrace) {
            return false;
        }

        // we allow cross track merging of staffs when they have the same program
        return this.track.playbackInfo.program === r.modelStaff.track.playbackInfo.program;
    }
}

/**
 * A StaffSystem consists of a list of different staves and groups
 * them using an accolade.
 * @internal
 */
export class StaffSystem {

    private _accoladeSpacingCalculated: boolean = false;

    private _brackets: SystemBracket[] = [];
    private _staffToBracket = new Map<RenderStaff, SystemBracket>();
    private _contentHeight = 0;

    private _hasSystemSeparator = false;

    public x: number = 0;
    public y: number = 0;
    public index: number = 0;

    /**
     * The width of the whole accolade inclusive text and bar.
     */
    public accoladeWidth: number = 0;

    /**
     * Indicates whether this line is full or not. If the line is full the
     * bars can be aligned to the maximum width. If the line is not full
     * the bars will not get stretched.
     */
    public isFull: boolean = false;

    /**
     * The width that the content bars actually need
     */
    public width: number = 0;
    public computedWidth: number = 0;
    public totalBarDisplayScale: number = 0;

    public isLast: boolean = false;
    public masterBarsRenderers: MasterBarsRenderers[] = [];
    public staves: StaffTrackGroup[] = [];
    public layout!: ScoreLayout;

    public topPadding: number;
    public bottomPadding: number;
    public allStaves: RenderStaff[] = [];

    public constructor(layout: ScoreLayout) {
        this.layout = layout;
        this.topPadding = layout.renderer.settings.display.systemPaddingTop;
        this.bottomPadding = layout.renderer.settings.display.systemPaddingBottom;
    }

    public get firstBarIndex(): number {
        return this.masterBarsRenderers[0].masterBar.index;
    }

    public get lastBarIndex(): number {
        return this.masterBarsRenderers[this.masterBarsRenderers.length - 1].lastMasterBarIndex;
    }

    public addMasterBarRenderers(tracks: Track[], renderers: MasterBarsRenderers): MasterBarsRenderers | null {
        if (tracks.length === 0) {
            return null;
        }
        this.masterBarsRenderers.push(renderers);
        renderers.layoutingInfo.preBeatSize = 0;
        let src: number = 0;
        for (let i: number = 0, j: number = this.staves.length; i < j; i++) {
            const g: StaffTrackGroup = this.staves[i];
            for (let k: number = 0, l: number = g.staves.length; k < l; k++) {
                const s: RenderStaff = g.staves[k];
                const renderer: BarRendererBase = renderers.renderers[src++];
                s.addBarRenderer(renderer);
            }
        }
        this._calculateAccoladeSpacing(tracks);

        this._updateWidthFromLastBar();
        return renderers;
    }

    public addBars(
        tracks: Track[],
        barIndex: number,
        additionalMultiBarRestIndexes: number[] | null
    ): MasterBarsRenderers {
        const result: MasterBarsRenderers = new MasterBarsRenderers();
        result.additionalMultiBarRestIndexes = additionalMultiBarRestIndexes;
        result.layoutingInfo = new BarLayoutingInfo();
        result.masterBar = tracks[0].score.masterBars[barIndex];
        this.masterBarsRenderers.push(result);

        // add renderers
        const barLayoutingInfo: BarLayoutingInfo = result.layoutingInfo;
        for (const g of this.staves) {
            for (const s of g.staves) {
                const bar: Bar = g.track.staves[s.modelStaff.index].bars[barIndex];

                const additionalMultiBarsRestBars: Bar[] | null =
                    additionalMultiBarRestIndexes == null
                        ? null
                        : additionalMultiBarRestIndexes.map(b => g.track.staves[s.modelStaff.index].bars[b]);

                s.addBar(bar, barLayoutingInfo, additionalMultiBarsRestBars);
                const renderer: BarRendererBase = s.barRenderers[s.barRenderers.length - 1];
                result.renderers.push(renderer);
                if (renderer.isLinkedToPrevious) {
                    result.isLinkedToPrevious = true;
                }
                if (!renderer.canWrap) {
                    result.canWrap = false;
                }
            }
        }
        this._calculateAccoladeSpacing(tracks);

        barLayoutingInfo.finish();
        // ensure same widths of new renderer
        result.width = this._updateWidthFromLastBar();

        return result;
    }

    public revertLastBar(): MasterBarsRenderers | null {
        if (this.masterBarsRenderers.length > 1) {
            const toRemove: MasterBarsRenderers = this.masterBarsRenderers[this.masterBarsRenderers.length - 1];
            this.masterBarsRenderers.splice(this.masterBarsRenderers.length - 1, 1);
            let width: number = 0;
            let barDisplayScale: number = 0;
            for (let i: number = 0, j: number = this.allStaves.length; i < j; i++) {
                const s: RenderStaff = this.allStaves[i];
                const lastBar: BarRendererBase = s.revertLastBar();
                const computedWidth = lastBar.computedWidth;
                if (computedWidth > width) {
                    width = computedWidth;
                }
                const newBarDisplayScale = lastBar.barDisplayScale;
                if (newBarDisplayScale > barDisplayScale) {
                    barDisplayScale = newBarDisplayScale;
                }
            }
            this.width -= width;
            this.computedWidth -= width;
            this.totalBarDisplayScale -= barDisplayScale;
            return toRemove;
        }
        return null;
    }

    private _updateWidthFromLastBar(): number {
        let realWidth: number = 0;
        let barDisplayScale: number = 0;
        for (let i: number = 0, j: number = this.allStaves.length; i < j; i++) {
            const s: RenderStaff = this.allStaves[i];

            const last = s.barRenderers[s.barRenderers.length - 1];
            last.applyLayoutingInfo();
            if (last.computedWidth > realWidth) {
                realWidth = last.computedWidth;
            }

            const newBarDisplayScale = last.barDisplayScale;
            if (newBarDisplayScale > barDisplayScale) {
                barDisplayScale = newBarDisplayScale;
            }
        }
        this.width += realWidth;
        this.computedWidth += realWidth;
        this.totalBarDisplayScale += barDisplayScale;

        return realWidth;
    }

    private _calculateAccoladeSpacing(tracks: Track[]): void {
        const settings = this.layout.renderer.settings;
        if (!this._accoladeSpacingCalculated) {
            this._accoladeSpacingCalculated = true;

            this.accoladeWidth = 0;

            const stylesheet = this.layout.renderer.score!.stylesheet;
            const hasTrackName = this.layout.renderer.settings.notation.isNotationElementVisible(
                NotationElement.TrackNames
            );

            if (hasTrackName) {
                const trackNamePolicy =
                    this.layout.renderer.tracks!.length === 1
                        ? stylesheet.singleTrackTrackNamePolicy
                        : stylesheet.multiTrackTrackNamePolicy;

                const trackNameMode =
                    this.index === 0 ? stylesheet.firstSystemTrackNameMode : stylesheet.otherSystemsTrackNameMode;

                const trackNameOrientation =
                    this.index === 0
                        ? stylesheet.firstSystemTrackNameOrientation
                        : stylesheet.otherSystemsTrackNameOrientation;

                let shouldRender = false;

                switch (trackNamePolicy) {
                    case TrackNamePolicy.Hidden:
                        break;
                    case TrackNamePolicy.FirstSystem:
                        shouldRender = this.index === 0;
                        break;
                    case TrackNamePolicy.AllSystems:
                        shouldRender = true;
                        break;
                }

                let hasAnyTrackName = false;
                if (shouldRender) {
                    const canvas: ICanvas = this.layout.renderer.canvas!;
                    const res: Font = settings.display.resources.effectFont;
                    canvas.font = res;
                    for (const t of tracks) {
                        let trackNameText = '';
                        switch (trackNameMode) {
                            case TrackNameMode.FullName:
                                trackNameText = t.name;
                                break;
                            case TrackNameMode.ShortName:
                                trackNameText = t.shortName;
                                break;
                        }

                        if (trackNameText.length > 0) {
                            hasAnyTrackName = true;
                            const size = canvas.measureText(trackNameText);
                            switch (trackNameOrientation) {
                                case TrackNameOrientation.Horizontal:
                                    this.accoladeWidth = Math.ceil(Math.max(this.accoladeWidth, size.width));
                                    break;
                                case TrackNameOrientation.Vertical:
                                    this.accoladeWidth = Math.ceil(Math.max(this.accoladeWidth, size.height));
                                    break;
                            }
                        }
                    }

                    if (hasAnyTrackName) {
                        this.accoladeWidth += settings.display.systemLabelPaddingLeft;
                        this.accoladeWidth += settings.display.systemLabelPaddingRight;
                    }
                }
            }

            // NOTE: we have a chicken-egg problem when it comes to scaling braces which we try to mitigate here:
            // - The brace scales with the height of the system
            // - The height of the system depends on the bars which can be fitted
            // By taking another bar into the system, the height can grow and by this the width of the brace and then it doesn't fit anymore.
            // It is not worth the complexity to align the height and width of the brace.
            // So we do a rough approximation of the space needed for the brace based on the staves we have at this point.
            // Additional Staff separations caused later are not respected.
            // users can mitigate truncation with specfiying a systemLabelPaddingLeft.

            // alternative idea for the future:
            // - we could force the brace to the width we initially calculate here so it will not grow beyond that.
            // - requires a feature to draw glyphs with a max-width or a horizontal stretch scale

            let currentY: number = 0;
            for (const staff of this.allStaves) {
                staff.y = currentY;
                staff.calculateHeightForAccolade();
                currentY += staff.height;
            }

            let braceWidth = 0;
            for (const b of this._brackets) {
                b.finalizeBracket(settings.display.resources.engravingSettings);
                braceWidth = Math.max(braceWidth, b.width);
            }

            this.accoladeWidth += braceWidth;

            this.width += this.accoladeWidth;
            this.computedWidth += this.accoladeWidth;
        }
    }

    private _getStaffTrackGroup(track: Track): StaffTrackGroup | null {
        for (let i: number = 0, j: number = this.staves.length; i < j; i++) {
            const g: StaffTrackGroup = this.staves[i];
            if (g.track === track) {
                return g;
            }
        }
        return null;
    }

    public addStaff(staff: RenderStaff): void {
        const track = staff.modelStaff.track;
        let group: StaffTrackGroup | null = this._getStaffTrackGroup(track);
        if (!group) {
            group = new StaffTrackGroup(this, track);
            this.staves.push(group);
        }
        staff.staffTrackGroup = group;
        staff.system = this;
        staff.index = this.allStaves.length;
        this.allStaves.push(staff);
        group.addStaff(staff);

        let bracket = this._brackets.find(b => b.includesStaff(staff));
        if (!bracket) {
            switch (track.score.stylesheet.bracketExtendMode) {
                case BracketExtendMode.NoBrackets:
                    break;
                case BracketExtendMode.GroupStaves:
                    // when grouping staves, we create one bracket for the whole track across all staves
                    bracket = new SingleTrackSystemBracket(track);
                    bracket.index = this._brackets.length;
                    this._brackets.push(bracket);
                    break;
                case BracketExtendMode.GroupSimilarInstruments:
                    bracket = new SimilarInstrumentSystemBracket(track);
                    bracket.index = this._brackets.length;
                    this._brackets.push(bracket);
                    break;
            }
        }

        if (bracket) {
            if (!bracket.firstStaffInBracket) {
                bracket.firstStaffInBracket = staff;
            }
            bracket.lastStaffInBracket = staff;
            // NOTE: one StaffTrackGroup can currently never have multiple brackets so we can safely keep the last known here
            group.bracket = bracket;
            this._staffToBracket.set(staff, bracket);
        }
    }

    public get height(): number {
        return Math.ceil(this._contentHeight + this.topPadding + this.bottomPadding);
    }

    public scaleToWidth(width: number): void {
        for (let i: number = 0, j: number = this.allStaves.length; i < j; i++) {
            this.allStaves[i].scaleToWidth(width);
        }
        this.width = width;
    }

    public paint(cx: number, cy: number, canvas: ICanvas): void {
        // const c = canvas.color;
        // canvas.color = Color.random(255);
        // canvas.strokeRect(cx + this.x, cy + this.y, this.width, this.height);
        // canvas.color = c;

        this.paintPartial(cx + this.x, cy + this.y + this.topPadding, canvas, 0, this.masterBarsRenderers.length);

        if (this._hasSystemSeparator) {
            using _ = ElementStyleHelper.track(
                canvas,
                TrackSubElement.SystemSeparator,
                this.allStaves[0].modelStaff.track
            );

            // NOTE: the divider is currently not "nicely" centered between the systems as this would lead to cropping

            // NOTE: Prevent cropping of separator if it overlaps
            const smuflMetrics = this.layout.renderer.settings.display.resources.engravingSettings;
            const overlap = Math.min(0, smuflMetrics.glyphBottom.get(MusicFontSymbol.SystemDivider) ?? 0);
            CanvasHelper.fillMusicFontSymbolSafe(
                canvas,
                cx + this.x,
                cy + this.y + this.height + overlap,
                1,
                MusicFontSymbol.SystemDivider,
                false
            );
            CanvasHelper.fillMusicFontSymbolSafe(
                canvas,
                cx + this.x + this.width - smuflMetrics.glyphWidths.get(MusicFontSymbol.SystemDivider)!,
                cy + this.y + this.height + overlap,
                1,
                MusicFontSymbol.SystemDivider,
                false
            );
        }
    }

    public paintPartial(cx: number, cy: number, canvas: ICanvas, startIndex: number, count: number): void {
        for (let i: number = 0, j: number = this.allStaves.length; i < j; i++) {
            this.allStaves[i].paint(cx, cy, canvas, startIndex, count);
        }
        const res: RenderingResources = this.layout.renderer.settings.display.resources;

        if (this.staves.length > 0 && startIndex === 0) {
            //
            // Draw start grouping
            //
            canvas.color = res.barSeparatorColor;

            //
            // Draw track names
            const settings = this.layout.renderer.settings;
            const hasTrackName = this.layout.renderer.settings.notation.isNotationElementVisible(
                NotationElement.TrackNames
            );
            canvas.font = res.effectFont;
            if (hasTrackName) {
                const stylesheet = this.layout.renderer.score!.stylesheet;

                const trackNamePolicy =
                    this.layout.renderer.tracks!.length === 1
                        ? stylesheet.singleTrackTrackNamePolicy
                        : stylesheet.multiTrackTrackNamePolicy;

                const trackNameMode =
                    this.index === 0 ? stylesheet.firstSystemTrackNameMode : stylesheet.otherSystemsTrackNameMode;

                const trackNameOrientation =
                    this.index === 0
                        ? stylesheet.firstSystemTrackNameOrientation
                        : stylesheet.otherSystemsTrackNameOrientation;

                let shouldRender = false;

                switch (trackNamePolicy) {
                    case TrackNamePolicy.Hidden:
                        break;
                    case TrackNamePolicy.FirstSystem:
                        shouldRender = this.index === 0;
                        break;
                    case TrackNamePolicy.AllSystems:
                        shouldRender = true;
                        break;
                }

                if (shouldRender) {
                    const oldBaseLine = canvas.textBaseline;
                    const oldTextAlign = canvas.textAlign;
                    for (const g of this.staves) {
                        if (g.staves.length > 0) {
                            const firstStart: number = cy + g.staves[0].contentTop;
                            const lastEnd: number = cy + g.staves[g.staves.length - 1].contentBottom;

                            let trackNameText = '';
                            switch (trackNameMode) {
                                case TrackNameMode.FullName:
                                    trackNameText = g.track.name;
                                    break;
                                case TrackNameMode.ShortName:
                                    trackNameText = g.track.shortName;
                                    break;
                            }

                            using _trackNameStyle = ElementStyleHelper.track(
                                canvas,
                                TrackSubElement.TrackName,
                                g.track
                            );

                            if (trackNameText.length > 0) {
                                const textEndX =
                                    // start at beginning of first renderer
                                    cx +
                                    g.staves[0].x -
                                    // left side of the bracket
                                    settings.display.accoladeBarPaddingRight -
                                    (g.bracket?.width ?? 0) -
                                    // padding between label and bracket
                                    settings.display.systemLabelPaddingRight;

                                switch (trackNameOrientation) {
                                    case TrackNameOrientation.Horizontal:
                                        canvas.textBaseline = TextBaseline.Middle;
                                        canvas.textAlign = TextAlign.Right;
                                        canvas.fillText(trackNameText, textEndX, (firstStart + lastEnd) / 2);
                                        break;
                                    case TrackNameOrientation.Vertical:
                                        canvas.textBaseline = TextBaseline.Bottom;
                                        canvas.textAlign = TextAlign.Center;

                                        // -90° looks terrible in chrome, antialiasing seems to be disabled
                                        // adding 0.1° to re-enable antialiasing at the cost of a slight angle
                                        const chromeTextAntialiasingFix = 0.1;

                                        canvas.beginRotate(
                                            textEndX,
                                            (firstStart + lastEnd) / 2,
                                            -90 - chromeTextAntialiasingFix
                                        );
                                        canvas.fillText(trackNameText, 0, 0);
                                        canvas.endRotate();
                                        break;
                                }
                            }
                        }
                    }
                    canvas.textBaseline = oldBaseLine;
                    canvas.textAlign = oldTextAlign;
                }
            }

            const needsSystemBarLine = !this.layout.renderer.score!.stylesheet.extendBarLines;
            if (this.allStaves.length > 0 && needsSystemBarLine) {
                let previousStaffInBracket: RenderStaff | null = null;
                for (const s of this.allStaves) {
                    if (previousStaffInBracket !== null) {
                        const previousBottom = previousStaffInBracket.contentBottom;
                        const thisTop = s.contentTop;

                        const accoladeX: number = cx + previousStaffInBracket.x;

                        const firstLineBarRenderer = previousStaffInBracket.barRenderers[0] as LineBarRenderer;

                        using _ = ElementStyleHelper.bar(
                            canvas,
                            firstLineBarRenderer.staffLineBarSubElement,
                            firstLineBarRenderer.bar
                        );
                        const h = Math.ceil(thisTop - previousBottom);
                        canvas.fillRect(accoladeX, cy + previousBottom, res.engravingSettings.thinBarlineThickness, h);
                    }

                    previousStaffInBracket = s;
                }
            }

            //
            // Draw brackets
            this._paintBrackets(cx, cy, canvas);
        }
    }

    private _paintBrackets(cx: number, cy: number, canvas: ICanvas) {
        const settings = this.layout.renderer.settings;

        for (const bracket of this._brackets!) {
            if (bracket.canPaint) {
                const barStartX: number = cx + bracket.firstStaffInBracket!.x;
                const barSize: number = bracket.width;
                const barOffset: number = settings.display.accoladeBarPaddingRight;
                const firstStart: number = cy + bracket.firstStaffInBracket!.contentTop;
                const lastEnd: number = cy + bracket.lastStaffInBracket!.contentBottom;
                let accoladeStart: number = firstStart;
                let accoladeEnd: number = lastEnd;

                if (bracket.drawAsBrace) {
                    CanvasHelper.fillMusicFontSymbolSafe(
                        canvas,
                        barStartX - barOffset - barSize,
                        accoladeEnd,
                        bracket.braceScale,
                        MusicFontSymbol.Brace
                    );
                } else if (bracket.firstStaffInBracket !== bracket.lastStaffInBracket) {
                    // brackets typically overflow by 1/4 staff-space
                    const smuflMetrics = settings.display.resources.engravingSettings;

                    const bracketOverflow = smuflMetrics.oneStaffSpace * 0.25;
                    accoladeStart -= bracketOverflow;
                    accoladeEnd += bracketOverflow;

                    // we shift the bar slightly inward so that the spike will hide the edge
                    // if we're precise we might see a slight light line on subpixel level
                    const barShift = 3;
                    canvas.fillRect(
                        barStartX - barOffset - barSize,
                        accoladeStart - barShift,
                        barSize,
                        Math.ceil(accoladeEnd - accoladeStart + barShift * 2)
                    );

                    const spikeX: number = barStartX - barOffset - barSize;
                    CanvasHelper.fillMusicFontSymbolSafe(canvas, spikeX, accoladeStart, 1, MusicFontSymbol.BracketTop);
                    CanvasHelper.fillMusicFontSymbolSafe(
                        canvas,
                        spikeX,
                        Math.floor(accoladeEnd),
                        1,
                        MusicFontSymbol.BracketBottom
                    );
                }
            }
        }
    }

    public finalizeSystem(): void {
        const settings = this.layout.renderer.settings;
        if (this.index === 0) {
            this.topPadding = settings.display.firstSystemPaddingTop;
        }

        if (this.isLast) {
            this.bottomPadding = settings.display.lastSystemPaddingBottom;
        } else if (
            this.layout.renderer.score!.stylesheet.useSystemSignSeparator &&
            this.layout.renderer.tracks!.length > 1
        ) {
            // NOTE: Reuse padding to place separato
            const neededHeight = settings.display.resources.engravingSettings.glyphHeights.get(
                MusicFontSymbol.SystemDivider
            )!;
            this.bottomPadding = Math.max(this.bottomPadding, neededHeight);
            this._hasSystemSeparator = true;
        }

        let currentY: number = 0;
        const smufl = settings.display.resources.engravingSettings;
        const topBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketTop)!;
        const bottomBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketBottom)!;

        let previousStaff: RenderStaff | undefined = undefined;

        for (const staff of this.allStaves) {
            // check if we need "in-between padding"
            if (previousStaff !== undefined && previousStaff!.trackIndex !== staff.trackIndex) {
                currentY += settings.display.trackStaffPaddingBetween;
            }

            const bracket = this._staffToBracket.has(staff) ? this._staffToBracket.get(staff) : undefined;
            const hasBracket = bracket && !bracket.drawAsBrace && bracket.canPaint;
            if (hasBracket && bracket!.firstStaffInBracket === staff) {
                const spikeOverflow = topBracketSpikeHeight - staff.topOverflow;
                if (spikeOverflow > 0) {
                    currentY += spikeOverflow;
                }
            }

            staff.x = this.accoladeWidth;
            staff.y = currentY;
            staff.finalizeStaff();
            currentY += staff.height;

            if (hasBracket && bracket!.lastStaffInBracket === staff) {
                const spikeOverflow = bottomBracketSpikeHeight - staff.bottomOverflow;
                if (spikeOverflow > 0) {
                    currentY += spikeOverflow;
                }
            }
            previousStaff = staff;
        }
        this._contentHeight = currentY;

        for (const b of this._brackets!) {
            b.finalizeBracket(smufl);
        }
    }

    public buildBoundingsLookup(cx: number, cy: number): void {
        if (this.layout.renderer.boundsLookup!.isFinished) {
            return;
        }
        const firstStaff = this.allStaves[0];
        const lastStaff = this.allStaves[this.allStaves.length - 1];

        cy += this.topPadding;

        const visualTop: number = cy + this.y + firstStaff.y;
        const visualBottom: number = cy + this.y + lastStaff.y + lastStaff.height;
        const visualHeight = visualBottom - visualTop;

        const realTop: number = cy + this.y;
        const realBottom: number = cy + this.y + this.height;
        const realHeight = realBottom - realTop;

        const lineTop = cy + this.y + firstStaff.y + firstStaff.topPadding + firstStaff.topOverflow;
        const lineBottom =
            cy + this.y + lastStaff.y + lastStaff.height - lastStaff.bottomPadding - lastStaff.bottomOverflow;
        const lineHeight = lineBottom - lineTop;

        const x: number = this.x + firstStaff.x;
        const staffSystemBounds = new StaffSystemBounds();
        staffSystemBounds.visualBounds = new Bounds();
        staffSystemBounds.visualBounds.x = cx + this.x;
        staffSystemBounds.visualBounds.y = cy + this.y;
        staffSystemBounds.visualBounds.w = this.width;
        staffSystemBounds.visualBounds.h = this.height - this.topPadding - this.bottomPadding;
        staffSystemBounds.realBounds = new Bounds();
        staffSystemBounds.realBounds.x = cx + this.x;
        staffSystemBounds.realBounds.y = cy + this.y;
        staffSystemBounds.realBounds.w = this.width;
        staffSystemBounds.realBounds.h = this.height;

        this.layout.renderer.boundsLookup!.addStaffSystem(staffSystemBounds);
        const masterBarBoundsLookup: Map<number, MasterBarBounds> = new Map<number, MasterBarBounds>();
        for (let i: number = 0; i < this.staves.length; i++) {
            for (const staff of this.staves[i].staves) {
                for (const renderer of staff.barRenderers) {
                    let masterBarBounds: MasterBarBounds;
                    if (!masterBarBoundsLookup.has(renderer.bar.masterBar.index)) {
                        masterBarBounds = new MasterBarBounds();
                        masterBarBounds.index = renderer.bar.masterBar.index;
                        masterBarBounds.isFirstOfLine = renderer.isFirstOfLine;
                        masterBarBounds.realBounds = new Bounds();
                        masterBarBounds.realBounds.x = x + renderer.x;
                        masterBarBounds.realBounds.y = realTop;
                        masterBarBounds.realBounds.w = renderer.width;
                        masterBarBounds.realBounds.h = realHeight;

                        masterBarBounds.visualBounds = new Bounds();
                        masterBarBounds.visualBounds.x = x + renderer.x;
                        masterBarBounds.visualBounds.y = visualTop;
                        masterBarBounds.visualBounds.w = renderer.width;
                        masterBarBounds.visualBounds.h = visualHeight;

                        masterBarBounds.lineAlignedBounds = new Bounds();
                        masterBarBounds.lineAlignedBounds.x = x + renderer.x;
                        masterBarBounds.lineAlignedBounds.y = lineTop;
                        masterBarBounds.lineAlignedBounds.w = renderer.width;
                        masterBarBounds.lineAlignedBounds.h = lineHeight;
                        this.layout.renderer.boundsLookup!.addMasterBar(masterBarBounds);
                        masterBarBoundsLookup.set(masterBarBounds.index, masterBarBounds);
                    } else {
                        masterBarBounds = masterBarBoundsLookup.get(renderer.bar.masterBar.index)!;
                    }
                    renderer.buildBoundingsLookup(masterBarBounds, x, cy + this.y + staff.y);
                }
            }
        }
    }

    public getBarX(index: number): number {
        if (this.allStaves.length === 0 || this.layout.renderer.tracks!.length === 0) {
            return 0;
        }
        const bar: Bar = this.layout.renderer.tracks![0].staves[0].bars[index];
        const renderer: BarRendererBase = this.layout.getRendererForBar(this.allStaves[0].staffId, bar)!;
        return renderer.x;
    }
}
